package com.wyz.socket.core.utils.buffer;
import java.io.*;

public class CircularCharBuffer {

    private final static int DEFAULT_SIZE = 1024;
    public final static int INFINITE_SIZE = -1;
    protected char[] buffer;
    protected volatile int readPosition = 0;
    protected volatile int writePosition = 0;
    protected volatile int markPosition = 0;
    protected volatile int markSize = 0;
    protected volatile boolean infinite = false;
    protected boolean blockingWrite = true;
    protected Reader reader = new CircularCharBufferReader();
    protected boolean readerClosed = false;
    protected Writer writer = new CircularCharBufferWriter();
    protected boolean writerClosed = false;
    public void clear(){
        synchronized (this){
            readPosition = 0;
            writePosition = 0;
            markPosition = 0;
            readerClosed = false;
            writerClosed = false;
        }
    }
    public Writer getWriter(){
        return writer;
    }
    public Reader getReader(){
        return reader;
    }
    public int getAvailable(){
        synchronized (this){
            return available();
        }
    }
    public int getSpaceLeft(){
        synchronized (this){
            return spaceLeft();
        }
    }
    public int getSize(){
        synchronized (this){
            return buffer.length;
        }
    }
    private void resize(){
        char[] newBuffer = new char[buffer.length * 2];
        int marked = marked();
        int available = available();
        if (markPosition <= writePosition){
            int length = writePosition - markPosition;
            System.arraycopy(buffer, markPosition, newBuffer, 0, length);
        } else {
            int length1 = buffer.length - markPosition;
            System.arraycopy(buffer, markPosition, newBuffer, 0, length1);
            int length2 = writePosition;
            System.arraycopy(buffer, 0, newBuffer, length1, length2);
        }
        buffer = newBuffer;
        markPosition = 0;
        readPosition = marked;
        writePosition = marked + available;
    }
    private int spaceLeft(){
        if (writePosition < markPosition){
            return (markPosition - writePosition - 1);
        }
        return ((buffer.length - 1) - (writePosition - markPosition));
    }
    private int available(){
        if (readPosition <= writePosition){
            // any space between the first read and
            // the first write is available.  In this case i
            // is all in one piece.
            return (writePosition - readPosition);
        }
        // space at the beginning and end.
        return (buffer.length - (readPosition - writePosition));
    }
    private int marked(){
        if (markPosition <= readPosition){
            return (readPosition - markPosition);
        }
        return (buffer.length - (markPosition - readPosition));
    }
    private void ensureMark(){
        if (marked() > markSize){
            markPosition = readPosition;
            markSize = 0;
        }
    }
    public CircularCharBuffer(){
        this (DEFAULT_SIZE, true);
    }
    public CircularCharBuffer(int size){
        this (size, true);
    }
    public CircularCharBuffer(boolean blockingWrite){
        this (DEFAULT_SIZE, blockingWrite);
    }
    public CircularCharBuffer(int size, boolean blockingWrite){
        if (size == INFINITE_SIZE){
            buffer = new char[DEFAULT_SIZE];
            infinite = true;
        } else {
            buffer = new char[size];
            infinite = false;
        }
        this.blockingWrite = blockingWrite;
    }

    protected class CircularCharBufferReader extends Reader {
        @Override public void close() throws IOException {
            synchronized (CircularCharBuffer.this){
                readerClosed = true;
            }
        }
        @Override public void mark(int readAheadLimit) throws IOException {
            synchronized (CircularCharBuffer.this){
                if (readerClosed) throw new IOException("Reader has been closed; cannot mark a closed Reader.");
                if (buffer.length - 1 <= readAheadLimit) throw new IOException("Cannot mark stream, readAheadLimit bigger than buffer size.");
                markSize = readAheadLimit;
                markPosition = readPosition;
            }
        }
        @Override public boolean markSupported() {
            return true;
        }
        @Override public int read() throws IOException {
            while (true){
                synchronized (CircularCharBuffer.this){
                    if (readerClosed) throw new IOException("Reader has been closed; cannot read from a closed Reader.");
                    int available = available();
                    if (available > 0){
                        int result = buffer[readPosition] & 0xffff;
                        readPosition++;
                        if (readPosition == buffer.length){
                            readPosition = 0;
                        }
                        ensureMark();
                        return result;
                    } else if (writerClosed){
                        return -1;
                    }
                }
                try {
                    Thread.sleep(100);
                } catch(Exception x){
                    throw new IOException("Blocking read operation interrupted.");
                }
            }
        }
        @Override public int read(char[] cbuf) throws IOException {
            return read(cbuf, 0, cbuf.length);
        }
        @Override public int read(char[] cbuf, int off, int len) throws IOException {
            while (true){
                synchronized (CircularCharBuffer.this){
                    if (readerClosed) throw new IOException("Reader has been closed; cannot read from a closed Reader.");
                    int available = available();
                    if (available > 0){
                        int length = Math.min(len, available);
                        int firstLen = Math.min(length, buffer.length - readPosition);
                        int secondLen = length - firstLen;
                        System.arraycopy(buffer, readPosition, cbuf, off, firstLen);
                        if (secondLen > 0){
                            System.arraycopy(buffer, 0, cbuf, off+firstLen,  secondLen);
                            readPosition = secondLen;
                        } else {
                            readPosition += length;
                        }
                        if (readPosition == buffer.length) {
                            readPosition = 0;
                        }
                        ensureMark();
                        return length;
                    } else if (writerClosed){
                        return -1;
                    }
                }
                try {
                    Thread.sleep(100);
                } catch(Exception x){
                    throw new IOException("Blocking read operation interrupted.");
                }
            }
        }
        @Override public boolean ready() throws IOException {
            synchronized (CircularCharBuffer.this){
                if (readerClosed) throw new IOException("Reader has been closed, it is not ready.");
                return (available() > 0);
            }
        }
        @Override public void reset() throws IOException {
            synchronized (CircularCharBuffer.this){
                if (readerClosed) throw new IOException("Reader has been closed; cannot reset a closed Reader.");
                readPosition = markPosition;
            }
        }
        @Override public long skip(long n) throws IOException, IllegalArgumentException {
            while (true){
                synchronized (CircularCharBuffer.this){
                    if (readerClosed) throw new IOException("Reader has been closed; cannot skip characters on a closed Reader.");
                    int available = available();
                    if (available > 0){
                        int length = Math.min((int)n, available);
                        int firstLen = Math.min(length, buffer.length - readPosition);
                        int secondLen = length - firstLen;
                        if (secondLen > 0){
                            readPosition = secondLen;
                        } else {
                            readPosition += length;
                        }
                        if (readPosition == buffer.length) {
                            readPosition = 0;
                        }
                        ensureMark();
                        return length;
                    } else if (writerClosed){
                        return 0;
                    }
                }
                try {
                    Thread.sleep(100);
                } catch(Exception x){
                    throw new IOException("Blocking read operation interrupted.");
                }
            }
        }
    }
    protected class CircularCharBufferWriter extends Writer {
        @Override public void close() throws IOException {
            synchronized (CircularCharBuffer.this){
                if (!writerClosed){
                    flush();
                }
                writerClosed = true;
            }
        }
        @Override public void flush() throws IOException {
            synchronized (CircularCharBuffer.this){
                if (writerClosed) throw new IOException("Writer has been closed; cannot flush a closed Writer.");
                if (readerClosed) throw new IOException("Buffer closed by Reader; cannot flush.");
            }
        }
        @Override public void write(char[] cbuf) throws IOException {
            write(cbuf, 0, cbuf.length);
        }
        @Override public void write(char[] cbuf, int off, int len) throws IOException {
            while (len > 0){
                synchronized (CircularCharBuffer.this){
                    if (writerClosed) throw new IOException("Writer has been closed; cannot write to a closed Writer.");
                    if (readerClosed) throw new IOException("Buffer closed by Reader; cannot write to a closed buffer.");
                    int spaceLeft = spaceLeft();
                    while (infinite && spaceLeft < len){
                        resize();
                        spaceLeft = spaceLeft();
                    }
                    if (!blockingWrite && spaceLeft < len) throw new BufferOverflowException("CircularCharBuffer is full; cannot write " + len + " characters");
                    int realLen = Math.min(len, spaceLeft);
                    int firstLen = Math.min(realLen, buffer.length - writePosition);
                    int secondLen = Math.min(realLen - firstLen, buffer.length - markPosition - 1);
                    int written = firstLen + secondLen;
                    if (firstLen > 0){
                        System.arraycopy(cbuf, off, buffer, writePosition, firstLen);
                    }
                    if (secondLen > 0){
                        System.arraycopy(cbuf, off+firstLen, buffer, 0, secondLen);
                        writePosition = secondLen;
                    } else {
                        writePosition += written;
                    }
                    if (writePosition == buffer.length) {
                        writePosition = 0;
                    }
                    off += written;
                    len -= written;
                }
                if (len > 0){
                    try {
                        Thread.sleep(100);
                    } catch(Exception x){
                        throw new IOException("Waiting for available space in buffer interrupted.");
                    }
                }
            }
        }
        @Override public void write(int c) throws IOException {
            boolean written = false;
            while (!written){
                synchronized (CircularCharBuffer.this){
                    if (writerClosed) throw new IOException("Writer has been closed; cannot write to a closed Writer.");
                    if (readerClosed) throw new IOException("Buffer closed by Reader; cannot write to a closed buffer.");
                    int spaceLeft = spaceLeft();
                    while (infinite && spaceLeft < 1){
                        resize();
                        spaceLeft = spaceLeft();
                    }
                    if (!blockingWrite && spaceLeft < 1) throw new BufferOverflowException("CircularCharBuffer is full; cannot write 1 character");
                    if (spaceLeft > 0){
                        buffer[writePosition] = (char)(c & 0xffff);
                        writePosition++;
                        if (writePosition == buffer.length) {
                            writePosition = 0;
                        }
                        written = true;
                    }
                }
                if (!written){
                    try {
                        Thread.sleep(100);
                    } catch(Exception x){
                        throw new IOException("Waiting for available space in buffer interrupted.");
                    }
                }
            }
        }
        @Override public void write(String str) throws IOException {
            write(str, 0, str.length());
        }
        @Override public void write(String str, int off, int len) throws IOException {
            while (len > 0){
                synchronized (CircularCharBuffer.this){
                    if (writerClosed) throw new IOException("Writer has been closed; cannot write to a closed Writer.");
                    if (readerClosed) throw new IOException("Buffer closed by Reader; cannot write to a closed buffer.");
                    int spaceLeft = spaceLeft();
                    while (infinite && spaceLeft < len){
                        resize();
                        spaceLeft = spaceLeft();
                    }
                    if (!blockingWrite && spaceLeft < len) throw new BufferOverflowException("CircularCharBuffer is full; cannot write " + len + " characters");
                    int realLen = Math.min(len, spaceLeft);
                    int firstLen = Math.min(realLen, buffer.length - writePosition);
                    int secondLen = Math.min(realLen - firstLen, buffer.length - markPosition - 1);
                    int written = firstLen + secondLen;
                    for (int i=0; i<firstLen; i++){
                        buffer[writePosition + i] = str.charAt(off+i);
                    }
                    if (secondLen > 0){
                        for (int i=0; i<secondLen; i++){
                            buffer[i] = str.charAt(off+firstLen+i);
                        }
                        writePosition = secondLen;
                    } else {
                        writePosition += written;
                    }
                    if (writePosition == buffer.length) {
                        writePosition = 0;
                    }
                    off += written;
                    len -= written;
                }
                if (len > 0){
                    try {
                        Thread.sleep(100);
                    } catch(Exception x){
                        throw new IOException("Waiting for available space in buffer interrupted.");
                    }
                }
            }
        }
    }
}
